Skip to content

Add some whimsical fun with an easter egg - #13152

Open
adamsilverstein wants to merge 11 commits into
WordPress:trunkfrom
adamsilverstein:65907-restore-easter-egg
Open

Add some whimsical fun with an easter egg#13152
adamsilverstein wants to merge 11 commits into
WordPress:trunkfrom
adamsilverstein:65907-restore-easter-egg

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Aug 18, 2026

Copy link
Copy Markdown
Member

Description

Adds an admin easter egg behind the command palette, picking up the Matrix scene WordPress used to play when you compared a revision to itself. That one shipped for about a decade and came out in [24820] (see #24852) when the revisions UI was rewritten and its doorway disappeared.

The command palette is the new doorway. No command is registered: a registered command shows up in the results the moment its phrase matches, which hands the egg to anyone who happens to type near it. Instead a keydown handler watches for Enter while the palette is open and acts on the phrase itself. The palette shows its ordinary "No results found." state throughout, so typing the phrase looks exactly like a search that missed.

That empty state is also the condition. The palette's search field is a combobox, so while any command matches, one of them is the active option and Enter belongs to it - the handler checks aria-activedescendant and stands down whenever the palette has something to run. Without that check, a post that happened to be titled with the phrase could never be opened from the palette, which @sabernhardt hit in https://core.trac.wordpress.org/ticket/65907#comment:5.

The phrase is compared through the same Dvorak/QWERTY substitution cipher the original used, so it is not a readable string in the admin bundle. It can be worked out from the source with a little effort, which seems like about the right amount.

The two files are named for what they do - teletype.js and teletype-loader.js - rather than for what they are. The loader is enqueued by name on every admin screen with the palette, so its src is right there in the markup, and a filename with "easter egg" in it gives away most of the fun before anyone goes looking. The docblocks still say plainly what this is; minification strips them, so they never reach a production page.

Trac ticket: https://core.trac.wordpress.org/ticket/65907

What changed after feedback on the ticket

@sabernhardt made a strong case that a faithful restoration lands badly, and most of it is addressed here:

  • The self-destruct countdown is gone. The site now announces a routine maintenance cycle and counts down to the Matrix instead, so nobody watching has reason to think they have been hacked.
  • "Self-comparison detected." no longer describes how the egg is reached. It is "Self-reference detected." now, which was @swissspidy's suggestion in review.
  • The closing line, "Don't let this happen again.", is dropped. The scene holds on the last Matrix line for a few seconds, then takes itself down and hands the admin page back.
  • The Matrix act now actually looks like the Matrix. The original was green text on black and nothing else, so the falling glyph rain everyone pictures is drawn on a canvas behind the dialogue. It sits out a prefers-reduced-motion: reduce preference.
  • There is a visible way out now: the blue pill-shaped Exit button, top right, present from the first frame. Escape and a click anywhere still work.
  • The overlay carries lang="en", since the dialogue is English whatever the profile language is set to.
  • Screen reader users get the egg rather than forty-five seconds of silence. See below.

Accessibility

The first version was aria-hidden end to end. That made the typing bearable, but it also meant a blind user pressed Enter and got nothing at all, while focus stayed on an admin page they could no longer see. Reworked:

  • The overlay is a role="dialog" with aria-modal="true", labelled "A WordPress easter egg. Press Escape to leave." It takes focus when it opens and hands focus back to wherever it came from on the way out.
  • The typing surface keeps aria-hidden, since a character at a time is not something anyone wants read to them. Each line is instead handed whole to a polite live region as it begins typing, so the narration keeps pace with the screen rather than trailing it.
  • The rain is described once, when the lights go out: "The screen goes black. Green code rains down it." The canvas itself is aria-hidden.
  • Tab is trapped on the Exit button, so nobody winds up tabbing an interface hidden behind the overlay.

Screenshots

Act one, before the lights go out:

Act one of the easter egg, black monospace text on a white overlay reading "Self-reference detected. Initiating infinite loop eschewal protocol. Running a routine maintenance cycle. Back in... 3 2 1"

The Matrix act, with the rain running behind the dialogue:

The Matrix act, green katakana falling in columns down a black screen with "Follow the white rabbit." typed in bright green at the top

Notes on the payload

Staging and timings follow the original. A few things needed updating after twelve years:

  • <blink> and text-decoration: blink are both gone from browsers. Replaced with a CSS @keyframes blink, plus a prefers-reduced-motion guard.
  • The original took over the document, rewriting html/body CSS and stripping nodes out of the page's first <p>. The scene now renders into its own overlay and touches nothing on the admin page.
  • The typed line and the rain canvas are both marked aria-hidden. Without that, text typed one character at a time lands in the accessibility tree one node at a time. The live region described above carries the dialogue instead.
  • Scrolling is locked while the scene plays. Otherwise the page behind it stays scrollable and leaves a scrollbar strip down the side.

Only a small loader is added to admin pages. The scene itself is fetched at the moment it is invoked, so a normal admin load carries nothing else.

How has this been tested

Test in WordPress Playground

  1. Open any admin screen and press Cmd/Ctrl+K.
  2. Type the phrase. The palette will say "No results found." the whole time, which is expected. Press Enter anyway.
  3. The scene runs for about 45 seconds and then clears itself. Esc or a click dismisses it at any point.

Worth a reviewer's eye, since the handler runs in capture phase:

  • Ordinary palette commands still run, including when the search text is the phrase itself. Publish a post titled with the phrase, search for it, and Enter should open the post rather than the egg.
  • Enter on a genuinely empty search does nothing.
  • The payload is not requested until the egg is invoked.
  • No console errors on normal admin screens.

The scene itself was driven in a standalone harness rather than in wp-admin, which is where the screenshots above come from. Confirmed there:

  • The rain canvas is created at the right moment, sized to the viewport, and the glyphs draw. No canvas is created at all when prefers-reduced-motion: reduce matches.
  • The live region receives all nine lines, in order, as whole lines - the three of act one, the countdown, the rain description, and the three Matrix lines.
  • Focus lands on the overlay when it opens and returns to the previously focused element on exit, whether the scene ends on its own or is dismissed with Escape.
  • Tab moves to the Exit button and stays there.
  • Scroll locking is undone and the injected style element is removed on every exit path.

The palette handler still wants a pass on a real admin screen, and the narration is worth hearing through an actual screen reader rather than trusting the live region contract.

grunt jshint:corejs, grunt typecheck:js, and phpcs on script-loader.php all pass on the current branch.

Types of changes

  • Add the scene as src/js/_enqueues/admin/teletype.js, fetched on demand.
  • Add the loader as src/js/_enqueues/admin/teletype-loader.js, enqueued with the command palette.
  • Register and enqueue the loader in wp_enqueue_command_palette_assets().
  • Add both files to the Gruntfile admin-js build map.
  • Draw the falling glyph rain on a canvas during the Matrix act, skipped under reduced motion.
  • Make the overlay a labelled modal dialog that manages focus, narrate whole lines through a live region, and add the Exit control.

AI Use

🤖 Claude Code wrote the code here and drafted this description, working from the discussion on the ticket. I will review and test.

The revisions easter egg was removed in [24820] because the revisions UI it hooked
into was rewritten and its trigger disappeared with it, not because the egg was
unwanted. This gives the payload a new home: a command palette loader that returns
no commands, and so stays invisible, until its phrase is typed.

The 2013 original shipped as a Dean Edwards packed blob. It is unpacked here into
readable source and no longer depends on jQuery, since obfuscated code is not GPL
source. See #15262. The trigger phrase is still compared through the original
Dvorak/QWERTY cipher so that it is not a readable string in the admin bundle; that
is the only obfuscation retained, and it is aimed at users rather than developers.

The payload is fetched only once the egg has been invoked, so a normal admin page
load carries nothing but the loader. It renders into a self-contained overlay
instead of taking over the document as the original did.

See #24852.
… admin easter egg.

Browser testing surfaced three problems with the overlay.

The page behind it stayed scrollable, so the overlay measured 15px narrower than
the viewport and left a visible scrollbar strip down the side. Scrolling is now
locked on the document element while the scene plays and restored when it is
dismissed.

The scene inserts text one character at a time, and each character landed in the
accessibility tree as its own node. The overlay is purely decorative, so it is now
marked aria-hidden.

Act two ended on an empty black screen with no indication of how to leave it. It
now closes on the line the original used as its noscript fallback, which both ends
the scene deliberately and hints that the page is still there.
JSHint enforces single-quoted strings in core JS, and jshint:core failed on this one
double-quoted literal. It was written that way only because the line contains an
apostrophe, so escape the apostrophe instead.
A registered command appears in the palette as soon as its phrase matches, so anyone
typing near the phrase is shown the egg rather than discovering it. Nothing is
registered now. A keydown handler watches for Enter while the palette is open and
acts on the phrase itself, which leaves the palette showing its ordinary "No results
found." state throughout, indistinguishable from a search that missed.

The handler runs in the capture phase, so it is guarded on the palette being open and
the phrase matching before it touches the event. Verified that ordinary palette
commands still run and that Enter on a genuinely empty search does nothing.

Also drops the now-unused label constant, since no label is rendered.
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props adamsilverstein, swissspidy.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@adamsilverstein

Copy link
Copy Markdown
Member Author

Calling the files easter-egg-* is probably a bit too obvious, maybe we can come up with something a bit less so.

The loader is enqueued by name on every admin screen that loads the command
palette, so its src appears in the markup of an ordinary page load. A file
called easter-egg-loader.js announces that something is hidden before anyone
goes looking, which is most of what the egg has to offer.

Name both files for what they do instead. The payload types lines onto a
fullscreen overlay, so teletype.js and teletype-loader.js describe the
mechanism while giving away nothing about the surprise. The script handle,
the config global, and the overlay class follow.

The docblocks still say plainly what this is. Minification strips them, so
they never reach a production page, and a core developer reading the source
should not have to guess.
@adamsilverstein

Copy link
Copy Markdown
Member Author

Agreed. Renamed to teletype.js and teletype-loader.js in b2150a3 - named for what they do rather than for what they are, since the loader's src shows up in the markup of every admin page that loads the palette. The script handle, config global and overlay class follow. Docblocks still say plainly what this is, and minification strips those so they never reach a production page.

@adamsilverstein adamsilverstein self-assigned this Aug 19, 2026
…logue.

The doc block credited WordPress 3.6, but 3.6 is where the egg was removed.
r8306 added it on July 10, 2008, when trunk was 2.6-beta3, so it first shipped
in 2.6 and survived through 3.5.x before r24820 dropped it.

Store the dialogue in its Dvorak/QWERTY-substituted form, as the original did,
so the payoff does not turn up in a plain-text search of wp-admin/js. The
decoder ships alongside it and all of the logic stays readable.
return;
}

var KEY = 'ishdg;rsdkot',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one 👍

Comment thread src/js/_enqueues/admin/teletype.js Outdated

/*
* The dialogue is stored in its Dvorak/QWERTY-substituted form and run back through
* dvortr() as each line is typed, exactly as the 2008 original stored it. The point is

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should include these kind of references ("exactly as the 2008 original stored it") in the code. It's more AI thinking out loud than helpful context.

Comment thread src/js/_enqueues/admin/teletype.js Outdated
Comment on lines +43 to +62
var ACT_ONE = [
'O.nu[jrmlapcorb e.y.jy.ev',
'Cbcycaycbi cbucbcy. nrrl .ojd.,an lpryrjrnv',
'O.nu e.oypgjy cbvvv 3',
'2',
'1'
];

// %s is replaced with the current user's display name.
var ACT_TWO = [
'<at. glw %ovvv',
'Yd. Maypcq dao frgvvv',
'Urnnr, yd. ,dcy. paxxcyv'
];

// Left on screen at the end, as the original's noscript fallback did.
var CLOSING = 'Erb-y n.y ydco dall.b aiacbv';

var FROM = '\',.pyfgcrl/=\\aoeuidhtns-;qjkxbmwvz"<>PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ[]',
TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-=';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i18n is ... challenging here 😄 Some good suggestions on the ticket about using lang attribute (and also the accessibility remarks)

Comment thread src/js/_enqueues/admin/teletype.js Outdated
Cut the provenance narrative and the rationale walkthroughs from the file
headers, leaving the facts a reader of the code needs. The history belongs
on the ticket.
…s own.

The self-comparison line no longer describes how the egg is reached, and a
self-destruct countdown risks reading as a compromised site. Announce a
routine maintenance cycle instead, and drop the closing line: the scene now
clears itself and hands the admin page back. See #65907.
The loader swallowed every Enter whose search text matched the phrase, so a
post titled with it could never be opened from the palette. Defer to the
active option whenever the palette has one. See #65907.
@adamsilverstein adamsilverstein changed the title Restore the revisions easter egg Add some whimsical fun with an easter egg Aug 22, 2026
…act.

The act was green text on black and nothing more, which is the part of the
reference everyone actually pictures. Draw the rain on a canvas behind the
dialogue, and sit it out when reduced motion is preferred. See #65907.
The scene was aria-hidden end to end, so a blind user got forty-five seconds
of nothing while focus sat on a page they could no longer see. Make it a
modal dialog that takes focus and gives it back, narrate whole lines through
a live region rather than the character-by-character surface, describe the
rain once, and add the visible Exit control that was missing for everyone.
See #65907.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants